﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;

using SharedGenomics.Core;

namespace SharedGenomics.DataAccess
{
    public class BeadStudioGenotypeFileReader : IBeadStudioGenotypeFileReader
    {
        public BeadStudioGenotypeFileReader(string filePath)
            : this(new FileStream(filePath, FileMode.Open, FileAccess.Read))
        {
            this.OwnsStream = true;
        }

        public BeadStudioGenotypeFileReader(Stream dataStream)
        {
            dataStream.ThrowIfNull("dataStream");
            ExceptionHelper.ThrowIf<ArgumentException>(!dataStream.CanSeek, "Stream must be seekable");

            StreamReader reader = new StreamReader(dataStream);
            string headerLine = reader.ReadLine();

            ExceptionHelper.ThrowIf<ArgumentException>(!reader.CurrentEncoding.Equals(Encoding.UTF8), "File must be encoded using UTF-8 encoding");

            //get individual names from header line
            this.GetIndividualNames(headerLine);

            //initialise line infos
            this.InitialiseDataLineInfos(dataStream);

            this.DataStream = dataStream;
        }

        public virtual IEnumerable<string> GetIndividualSamples(int index)
        {
            index.ThrowIfNotBetween(0, this.HeaderItemList.Count - 1, true, "index");

            byte[] buffer = new byte[2];
            Encoding enc = Encoding.UTF8;

            foreach (var lineInfo in this.LineInfos)
            {
                //find the offset in the stream of the start of the current sample
                //this position should be the tab character just before the two sample characters
                long tabOffset = lineInfo.startPosition + lineInfo.snpNameLength + 3 * index;
                this.DataStream.Seek(tabOffset+1, SeekOrigin.Begin);
                this.DataStream.Read(buffer, 0, 2);

                yield return new String(enc.GetChars(buffer));
            }
        }

        private void GetIndividualNames(string headerLine)
        {
            this.HeaderItemList = new List<string>(headerLine.Split('\t').Skip(1));

            //there should be 3 characters for each sample for each header item - one tab and two chars containing the genotype at that locus
            this.LineFixedDataLength = 3 * this.HeaderItemList.Count;
        }

        private void InitialiseDataLineInfos(Stream dataStream)
        {
            Debug.Assert(dataStream != null);
            Debug.Assert(dataStream.CanSeek);

            this.LineInfos = new List<DataLineInfo>();

            Encoding enc = Encoding.UTF8;
            byte cr = enc.GetBytes("\r")[0];
            byte nl = enc.GetBytes("\n")[0];
            byte tab = enc.GetBytes("\t")[0];

            //find the start of the data (i.e. end of the header row and newline characters) and move the file pointer to that postion
            long dataStartPosition = this.GetDataStartPosition(dataStream, cr, nl);
            ExceptionHelper.ThrowIf<OutOfMemoryException>(dataStartPosition == -1, "Buffer too small to contain header line data");

            //each line consists of a variable-length snp name and a fixed length data section (and some newline characters)
            //once the length of each snp name is known then the end of the line can be calculated and the start of the next line found.
            //when the positions of the starts of all lines are known then the data can be accessed randomly

            long currentStartPosition = dataStartPosition;
            int currentTabOffset;
            long streamLength = dataStream.Length;
            byte[] buffer = new byte[256];      //buffer only needs to be long enough for the the snp name and trailing tab separator
            int bytesRead;
            
            while (currentStartPosition < streamLength)
            {
                //move to the next line start position and read into the buffer
                dataStream.Seek(currentStartPosition, SeekOrigin.Begin);
                
                //find first tab position in the buffer
                bytesRead = dataStream.Read(buffer, 0, buffer.Length);
                currentTabOffset = -1;
                while(buffer[++currentTabOffset] != tab);

                String snpName = new String(enc.GetChars(buffer, 0, currentTabOffset));
                DataLineInfo lineInfo = new DataLineInfo() { startPosition = currentStartPosition, snpName = snpName };
                this.LineInfos.Add(lineInfo);

                //length of this line should be variable + fixed + 2 (2 newline characters)
                currentStartPosition += currentTabOffset + this.LineFixedDataLength + 2;
            }
        }

        private long GetDataStartPosition(Stream dataStream, byte cr, byte nl)
        {
            byte[] buffer = new byte[100000];
            dataStream.Seek(0, SeekOrigin.Begin);
            int bytesRead = dataStream.Read(buffer, 0, buffer.Length);

            bool previousNl = false;
            bool isNl = false;
            byte current;
            for (int i = 0; i < bytesRead; i++)
            {
                current = buffer[i];
                isNl = current == cr || current == nl;

                if (previousNl && !isNl)
                    return i;

                previousNl = isNl;
            }

            //if the first newline has not been found and the entire stream has been read into the buffer then there is only one line so return where the data would start in the stream
            if (isNl && bytesRead <= buffer.Length)
                return bytesRead + 1;

            //unable to find the start of the data
            return -1;
        }

        public ReadOnlyCollection<string> HeaderItems
        {
            get { return new ReadOnlyCollection<string>(this.HeaderItemList); }
        }

        public IEnumerable<string> MarkerNames
        {
            get
            {
                return this.LineInfos.Select(li => li.snpName);
            }
        }

        private bool OwnsStream { get; set; }
        private Stream DataStream { get; set; }
        private IList<string> HeaderItemList { get; set; }
        private IList<DataLineInfo> LineInfos { get; set; }
        private int LineFixedDataLength { get; set; }

        #region IDisposable Members

        public void Dispose()
        {
            if (this.OwnsStream)
            {
                this.DataStream.Dispose();
            }
        }

        #endregion

        #region DataLineInfo

        private struct DataLineInfo
        {
            public long startPosition;
            public string snpName;
            public int snpNameLength
            {
                get { return this.snpName.Length; }
            }
        }

        #endregion
    }
}
